/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.me.sd;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 *
 * @author jorge
 */
public class Parser {

    public final static int A_VALUE = 'a';
    public final static int MAX_CHAR = 26;
    public final static int MAX_BUFFER_SIZE = 1024*256;
    public final static int HEADER_SIZE = 8;

    /** Gerador de números aleatórios*/
    public final static Random randomGenerator = new Random(1);

    /** conjuntos possiveis */
    private static Map<Short, StatNode> map;
    /** array com ids de inicios válidos*/
    private static short[] initsIds;
    /** array com probabilidades de inicios válidos */
    private static short[] initsValues;


    /**
     * Gera um short aleatório e pesquisa-o no vector de valores iniciais
     * returnando o index o elemento onde aparece esse valor
     *
     *
     * @return index no vector initValues
     * @throws IOException 
     */
    public static int getRandomInit() {
        short random = Parser.getNextShort();
        int index = Arrays.binarySearch(initsValues, random);
        if (index < 0) {
            index = index * (-1) - 1;
        }
        return index;
    }


    /**
     * Gera um valor aleatório, pesquisa por esse valor no vector de initValues
     * e retorna o SimpleNode associado ao indice
     *
     * @param channel
     * @param buffer
     * @return
     * @throws IOException
     */
    public static SimpleNode getRandomInit(FileChannel channel, ByteBuffer buffer) throws IOException {
        int index = getRandomInit();
        return Parser.getNode(channel, buffer, map.get(initsIds[index]).getPosition(), null);
    }


    public interface  IGeneratorListener{
        public void onNewChar(int size);
    }

    /**
     * Método geral para a geração de passwords
     *
     * @param size
     * @param kValue
     * @param channel
     * @param bb
     * @return
     * @throws IOException
     */
    public static byte[] generatePass(int size, int kValue, FileChannel channel, ByteBuffer bb,IGeneratorListener listener) throws IOException {
        byte[] word = new byte[size];
        //Gerar inicio
        int index = getRandomInit();  //Valor Aleatório
        short initChars = initsIds[index]; //short inicial (2 chars)
        int location = map.get(initChars).getPosition(); //localização do nó

        byte init[] = Parser.parseToByte(initChars);
        word[0] =  init[0];       //Caracteres iniciais
        listener.onNewChar(0);
        word[1] =  init[1];
        listener.onNewChar(1);

        SimpleNode node = Parser.getNode(channel, bb, location, null);
        for (int i = 2; i < kValue; i++) {
            boolean b = false;
            index = Parser.getNextShort();
            for (int j = 0; j < node.getStats().length; j++) {
                StatNode sn = node.getStats()[j];
                if (sn != null && sn.getBeginStat() >= index) {
                    word[i] = (byte) j;
                    listener.onNewChar(i);
                    location = sn.getPosition();
                    b=true;
                    break;
                }
            }
            if(!b){
                throw  new IOException();
            }
            if(i!=kValue-1){
                node = Parser.getNode(channel, bb, location, null);
            }
        }

        byte[] ini;
        byte[] rest;
        StatNode stat;

        for (int i = kValue; i < size - 1; i++) {
            ini = subArray(word, i -kValue, 2);
            rest = subArray(word, i-kValue+2, kValue - 2);

            short s = Parser.parseToShort(ini);

            stat = map.get(s);
            node = Parser.getNode(channel, bb, stat.getPosition(), rest);
            index = Parser.getNextShort();
            boolean b =false;
            for (int j = 0; j < node.getStats().length; j++) {
                StatNode sn = node.getStats()[j];
                if (sn != null && sn.getMiddleStat() >= index) {
                    word[i] = (byte) j;
                    listener.onNewChar(i);
                    b = true;
                    break;
                }
            }
            if(!b){
                throw new IOException();
            }
        }

        ini = subArray(word, word.length - kValue - 1, 2);
        rest = subArray(word, word.length - kValue+1, kValue - 2);

        short s = Parser.parseToShort(ini);
        stat = map.get(s);
        node = Parser.getNode(channel, bb, stat.getPosition(), rest);
        index = Parser.getNextShort();
        boolean b = false;
        for (int j = 0; j < node.getStats().length; j++) {
            StatNode sn = node.getStats()[j];
            if (sn != null && sn.getEndStat() >= index) {
                word[word.length - 1] = (byte)j;
                listener.onNewChar(word.length - 1);
                b=true;
                break;
            }
        }
        if(!b){
            throw new IOException();
        }
        return word;
    }


    /**
     *
     * Faz Um slip do arra de bytes
     *
     * @param bs
     * @param offset
     * @param limit
     * @return
     */
    private static byte[] subArray(byte bs[], int offset, int limit) {
        byte bb[] = new byte[limit];
        int j = 0;
        for (int i = offset; i < limit+offset; i++) {
            bb[j++] = bs[i];
        }
        return bb;
    }



    /**
     * Calcula um sub array de char com os limites passados
     *
     * @param palavra
     * @param inicio
     * @param fim
     * @return
     */
    public static char[] subChar(char[] palavra, int inicio, int fim) {
        int tamanho = fim - inicio;
        char c[] = new char[tamanho];
        for (int i = inicio; i < fim; i++) {
            c[i - inicio] = palavra[i];
        }
        return c;
    }


    /**
     * Calcula as estatisticas de uma palavra e coloca-as no map
     * Testa combinações de 2 até dimensão da palavra
     * 
     * @param string
     * @param map
     */
    public static void parseWord(String string, Map<Short, ParserNode> map) {
        char word[] = string.toCharArray();
        char conjunto[];
        char inicio[], fim[];
        for (int i = 2; i <= word.length; i++) { //Combinações de 2 até ao tamanho da palavra
            for (int j = 0; j <= word.length - i; j++) { //Tratamento de de cada combinação
                conjunto = subChar(word, j, j + i);
                inicio = subChar(conjunto, 0, 2);
                fim = subChar(conjunto, 2, conjunto.length);
                short key = parseToShort(inicio);
                ParserNode node = map.get(key);
                if (node == null) {
                    node = new ParserNode();
                    map.put(key, node);
                }
                boolean isInicio, isFim;
                isFim = j == word.length - i;   //j == word.legth - i -1 é fim
                isInicio = j == 0;
                node.put(new String(inicio), fim, isInicio, isFim, !isFim && !isInicio);
            }
        }

    }

    /**
     * Função para gerar ficheiro de estatisticas
     * 
     * @param args
     */
    public static void main(String[] args) {
        Map<Short, ParserNode> m = new HashMap<Short, ParserNode>();
        try {
            Map<Short, ParserNode> map = parseFile(new File("c://t//dic.txt"));
            save(map, new File("c://t//teste.bin"));
           
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * Função para fazer parse de um ficheiro
     *
     * @param file
     * @return
     * @throws IOException
     */
    public static Map<Short, ParserNode> parseFile(File file) throws IOException {
        Map<Short, ParserNode> map = new HashMap<Short, ParserNode>();
        BufferedReader dis = new BufferedReader(new FileReader(file));
        String st = null;
        while ((st = dis.readLine()) != null) {
            parseWord(st, map);
        }
        return map;
    }

    /**
     * Método para guardar as estatisticas em ficheiro
     *
     * @param map
     * @param file
     * @throws IOException
     */
 public static void save(Map<Short, ParserNode> map, File file) throws IOException {
        if (file.exists()) {
            file.delete();
        }
        file.createNewFile();
        RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
        FileChannel channel = accessFile.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(MAX_BUFFER_SIZE);
        int positions[] = new int[map.size()];

        int totalValidos = 0;
        int totalValidosInicio = 0;
        int totaisInicio = 0;
        int totalValidosFim = 0;
        int totaisFim = 0;
        int totalValidosMeio = 0;
        int totaisMeio = 0;


        for (Map.Entry<Short, ParserNode> entry : map.entrySet()) {
            ParserNode node = entry.getValue();
            totalValidos++;
            if (node.getNumInicios() > 0) {
                totalValidosInicio++;
                totaisInicio += node.getNumInicios();
            }
            if (node.getNumFins() > 0) {
                totalValidosFim++;
                totaisFim += node.getNumFins();
            }
            if (node.getNumMeios() > 0) {
                totalValidosMeio++;
                totaisMeio += node.getNumMeios();
            }
        }


        short acumuladorInicio = 0, acumuladorMeio = 0, acumuladorFim = 0;

        short lastAcumuladorInicio = 0, lastAcumuladorMeio = 0, lastAcumuladorFim = 0;
        int totaisInicioAux = 0,totaisMeioAux=0,totaisFimAux =0;

        for (Map.Entry<Short, ParserNode> entry : map.entrySet()) {
            ParserNode node = entry.getValue();
            if (node.getNumInicios() > 0) {
                lastAcumuladorInicio =acumuladorInicio;
                acumuladorInicio += normalizeToShort(node.getNumInicios() / (float) totaisInicio);
                if(acumuladorInicio==lastAcumuladorInicio){
                    node.setNumInicios(0);
                }else{
                    totaisInicioAux++;
                }
                lastAcumuladorInicio  =acumuladorInicio;
            }
            if (node.getNumMeios() > 0) {
                lastAcumuladorMeio =acumuladorMeio;
                acumuladorMeio += normalizeToShort(node.getNumMeios() / (float) totaisMeio);
                if(acumuladorMeio==lastAcumuladorMeio){
                    node.setNumMeios(0);
                }else{
                    totaisMeioAux++;
                }
                lastAcumuladorMeio  =acumuladorMeio;
            }
            if (node.getNumFins() > 0) {
                lastAcumuladorFim =acumuladorFim;
                acumuladorFim += normalizeToShort(node.getNumFins() / (float) totaisFim);
                if(acumuladorFim==lastAcumuladorFim){
                    node.setNumFins(0);
                }else{
                    totaisFimAux++;
                }
                lastAcumuladorFim  =acumuladorFim;
            }
        }

        totalValidosInicio = totaisInicioAux;
        totalValidosMeio = totaisMeioAux;
        totalValidosFim = totaisFimAux;


        acumuladorInicio = 0; acumuladorMeio = 0; acumuladorFim = 0;
        totaisInicioAux = 0; totaisMeioAux = 0;totaisFimAux = 0;

        int i=0;
        buffer.putShort((short)map.size());
        buffer.putShort((short)totalValidosInicio);
        for (Map.Entry<Short,ParserNode> entry : map.entrySet()) {
            ParserNode node = entry.getValue();
                buffer.putShort(entry.getKey());
                positions[i] = buffer.position();
                buffer.putInt(0);
                if (node.getNumInicios() > 0) {
                    totaisInicioAux++;
                    acumuladorInicio += normalizeToShort(node.getNumInicios() / (float) totaisInicio);
                    if (totaisInicioAux == totalValidosInicio) {
                        buffer.putShort(Short.MAX_VALUE);
                    } else {
                        buffer.putShort(acumuladorInicio);
                    }
                } else {
                    buffer.putShort((short) 0);
                }
                if (node.getNumMeios() > 0) {
                    totaisMeioAux++;
                    acumuladorMeio += normalizeToShort(node.getNumMeios() / (float) totaisMeio);
                    if (totaisMeioAux == totalValidosMeio) {
                        buffer.putShort(Short.MAX_VALUE);
                    } else {
                        buffer.putShort(acumuladorMeio);
                    }
                } else {
                    buffer.putShort((short) 0);
                }
                if (node.getNumFins() > 0) {
                    totaisFimAux++;
                    acumuladorFim += normalizeToShort(node.getNumFins() / (float) totaisFim);
                    if (totaisFimAux == totalValidosFim) {
                        buffer.putShort(Short.MAX_VALUE);
                    } else {
                        buffer.putShort(acumuladorFim);
                    }
                } else {
                    buffer.putShort((short) 0);
                }
                i++;
        }
        buffer.flip();
        while(buffer.hasRemaining()){
            channel.write(buffer);
        }

        int position [] = new int[map.size()];

        i = 0;
        for (Map.Entry<Short, ParserNode> entry : map.entrySet()) {
            ParserNode node = entry.getValue();
            if (node.getNumChildren() > 0) {
                position[i] = saveNode(channel, entry.getValue());
            }
            i++;
        }

        System.out.println(" header : "+map.size());
        i = 0;
        long current = channel.position();
        for (Map.Entry<Short, ParserNode> entry : map.entrySet()) {
            ParserNode node = entry.getValue();
            buffer.clear();
            buffer.putInt(position[i]);
            buffer.flip();
            channel.position(positions[i]);
            channel.write(buffer);
            i++;
        }
        channel.position(current);

        channel.force(true);
        channel.close();
        accessFile.close();
    }


    /**
     * Guarda um nó para ficheiro
     *
     * @param channel
     * @param node
     * @return
     * @throws IOException
     */
    public static int saveNode(FileChannel channel, ParserNode node) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(MAX_BUFFER_SIZE);

        int result = ((Long) channel.position()).intValue();
        buffer.put((byte) node.getNumChildren()); // n.º de filhos

        int pos[] = saveStats(node.getNos(), buffer);

        buffer.flip();

        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }
        int[] ids = new int[MAX_CHAR];
        for (int i = 0; i < node.getNos().length; i++) {
            ParserNode parserNode = node.getNos()[i];
            if (parserNode != null) {
                ids[i] = saveNode(channel, parserNode);
            }
        }

        long current = channel.position();
        for (int i = 0; i < node.getNos().length; i++) {
            if (node.getNos()[i] != null) {
                buffer.clear();
                buffer.putInt(ids[i]);
                buffer.flip();
                channel.position(pos[i] + result);
                channel.write(buffer);
            }
        }
        channel.position(current);

        return result;
    }

    /**
     * Guarda estados associados a um nó
     * 
     * @param nodes
     * @param bb
     * @return
     */
    public static int[] saveStats(ParserNode[] nodes, ByteBuffer bb) {

        int ids [] = new int[MAX_CHAR];

        int totalValidos = 0;
        int totalValidosInicio = 0;
        int totaisInicio = 0;
        int totalValidosFim = 0;
        int totaisFim = 0;
        int totalValidosMeio = 0;
        int totaisMeio = 0;

        for (int i = 0; i < nodes.length; i++) {
            ParserNode node = nodes[i];
            if (node != null) {
                totalValidos++;
                if (node.getNumInicios() > 0) {
                    totalValidosInicio++;
                    totaisInicio += node.getNumInicios();
                }
                if (node.getNumFins() > 0) {
                    totalValidosFim++;
                    totaisFim += node.getNumFins();
                }
                if (node.getNumMeios() > 0) {
                    totalValidosMeio++;
                    totaisMeio += node.getNumMeios();
                }
            }
        }

        short acumuladorInicio = 0, acumuladorMeio = 0, acumuladorFim = 0;
        int totaisInicioAux = 0, totaisMeioAux = 0, totaisFimAux = 0;

        for (int i = 0; i < nodes.length; i++) {
            ParserNode node = nodes[i];
            if (node != null) {
                bb.put((byte) i);
                ids[i] = bb.position();
                bb.putInt(0);
                if (node.getNumInicios() > 0) {
                    totaisInicioAux++;
                    acumuladorInicio += normalizeToShort(node.getNumInicios() / (float) totaisInicio);
                    if (totaisInicioAux == totalValidosInicio) {
                        bb.putShort(Short.MAX_VALUE);
                    } else {
                        bb.putShort(acumuladorInicio);
                    }
                }else{
                    bb.putShort((short)0);
                }
                if (node.getNumMeios() > 0) {
                    totaisInicioAux++;
                    acumuladorMeio += normalizeToShort(node.getNumMeios() / (float) totaisMeio);
                    if (totaisMeioAux == totalValidosMeio) {
                        bb.putShort(Short.MAX_VALUE);
                    } else {
                        bb.putShort(acumuladorMeio);
                    }
                }else{
                    bb.putShort((short)0);
                }
                if (node.getNumFins() > 0) {
                    totaisFimAux++;
                    acumuladorFim += normalizeToShort(node.getNumFins() / (float) totaisFim);
                    if (totaisFimAux == totalValidosFim) {
                        bb.putShort(Short.MAX_VALUE);
                    } else {
                        bb.putShort(acumuladorFim);
                    }
                }else{
                    bb.putShort((short)0);
                }
            }
        }
        return ids;
    }


    /**
     *
     * inicializa o header do ficheiro
     *
     * @param channel
     * @param bb
     * @param listener
     * @throws IOException
     */
    public static void initHeader(FileChannel channel, ByteBuffer bb,IHeaderNodeParser listener) throws IOException{
        map = new HashMap<Short, StatNode>();
        if(bb.limit()-bb.position()<4){
            realocate(bb, bb.position());
            channel.read(bb);
            bb.flip();
        }
        short nodesSize = bb.getShort();
        short initsSize = bb.getShort();
        
        initsIds = new short[initsSize];
        initsValues = new short[initsSize];
        
        listener.onFirstHeaderNode(nodesSize,initsSize);

        int generator = 0;
        for(int i = 0; i<nodesSize;i++){
            if(bb.limit()-bb.position()<12){
                realocate(bb, bb.position());
                channel.read(bb);
                bb.flip();
            }
            short sh = bb.getShort();
            StatNode node = new StatNode(bb.getInt(), bb.getShort(), bb.getShort(), bb.getShort());
            map.put(sh, node);
            if (node.getBeginStat() > 0) {
                                initsIds[generator] = sh;
                                initsValues[generator++] = node.getBeginStat();
            }
            listener.onParseHeaderNode(i+1);
        }

    }


    
    /**
     *
     * carrega os nós do canal e lança eventos para o listener
     *
     * @param channel
     * @param bb
     * @param listener
     * @throws IOException
     */
    public static void getNodes(FileChannel channel,ByteBuffer bb,INodeParser listener) throws IOException {
        try {
            while(channel.position()!=channel.size()){
                listener.onParseNode(getNode(channel,bb),((Long)channel.position()).intValue());
            }
        } finally {
            channel.close();
        }
    }

    /**
     * Realoca o bytebuffer para adicionar novos elementos
     * 
     * @param bb
     * @param position
     */
    private static void realocate(ByteBuffer bb , int position){
        int limit = bb.limit(),j=0;
        for(int i = position ; i<limit;i++,j++){
            bb.put(j, bb.get(i));
        }
        bb.position(j);
    }

    /**
     * Obtem um SimpleNode armazenado no ficheiro
     *
     * o bytebuffer já deve estar alinhado
     *
     * @param channel
     * @param bb
     * @return
     * @throws IOException
     */
    public static SimpleNode getNode(FileChannel channel,ByteBuffer bb) throws IOException {
        SimpleNode node = new SimpleNode();
        if(bb.limit()-bb.position()<1){
            realocate(bb, bb.position());
            channel.read(bb);
            bb.flip();
        }
        int children = bb.get();

        for(int i =0 ; i<children;i++){
            if(bb.limit()-bb.position()<14){
                realocate(bb, bb.position());
                channel.read(bb);
                bb.flip();
            }
            node.put(bb.get(), bb.getInt(), bb.getShort(), bb.getShort(), bb.getShort());
        }
        
        return node;
    }

    /**
     * Obtem um Simple node através da cadeia de bytes de entrada
     *
     * Inicializa o bytebuffer
     *
     * @param channel
     * @param bb
     * @param init
     * @param bs
     * @return
     * @throws IOException
     */
    public static SimpleNode getNode(FileChannel channel,ByteBuffer bb, int init , byte [] bs)throws IOException{
        bb.position(bb.capacity());
        channel.position(init);
        SimpleNode node = getNode(channel, bb);
        if(bs==null || bs.length==0){
            return node;
        }
        int next = node.getStatNode(bs[0]).getPosition();
        return getNode(channel, bb, next,pop(bs));
    }

    /**
     * Retira um byte no array de entrada
     * 
     * @param bs
     * @return
     */
    public static byte [] pop(byte  [] bs){
        byte [] bs1 = new byte[bs.length-1];
        for(int i = 1;i<bs.length;i++ ){
            bs1[i-1]= bs[i];
        }
        return bs1;
    }

    /**
     * Gerador de shorts aleatório
     * 
     * @return
     */
    public static short getNextShort(){
        return (short)((Float)randomGenerator.nextFloat()*Short.MAX_VALUE);
    }


    /**
     * Converte uma String de 2 elementos para short
     * Seja a A = "xy", short = (x-'a')*26+ (y-'a')
     *
     * @param string
     * @return
     */
    public static short parseToShort(String string) {
        return parseToShort(string.toCharArray());
    }

    /**
     * Converte um vector com 2 elementos para String
     *
     * @param string
     * @return
     */
    public static String parseToString(byte  [] bs){
        char [] cs =  new char[bs.length];
                    for(int i = 0;i< bs.length;i++){
                        cs[i]=(char)(bs[i]+Parser.A_VALUE);
                    }
        return new String(cs);
    }

    /**
     * Converte um vector com 2 elementos para short
     * Seja a A = [a,a], short = (x-'a')*26+ (y-'a')
     *
     * @param string
     * @return
     */
    public static short parseToShort(char[] letras) {
        int val = letras[0] - A_VALUE;
        int val1 = letras[1] - A_VALUE;
        return (short) (((short) val * MAX_CHAR) + val1);
    }


    /**
     * Converte um vector com 2 elementos para short
     * Seja a A = [a,a], short = (x-'a')*26+ (y-'a')
     *
     * @param string
     * @return
     */
    public static short parseToShort(byte[] letras) {
        int val = letras[0];
        int val1 = letras[1];
        return (short) (((short) val * MAX_CHAR) + val1);
    }

    /**
     * Converte um short para string
     *
     * @param num
     * @return
     */
    public static String parseToString(short num) {
        char[] chars = new char[2];
        chars[0] = (char) (num / MAX_CHAR + A_VALUE);
        chars[1] = (char) (num % MAX_CHAR + A_VALUE);
        return String.valueOf(chars);
    }

    /**
     * Converte um short para byte
     *
     * @param num
     * @return
     */
    public static byte[] parseToByte(short num) {
        byte[] chars = new byte[2];
        chars[0] = (byte) (num / MAX_CHAR);
        chars[1] = (byte) (num % MAX_CHAR);
        return chars;
    }

    /**
     *  Normaliza um valor entre 0 e 1 para 0 e Short.MAX_VALUE
     *
     * 0 <= f <= 1
     *
     * @param f
     * @return
     */
    public static short normalizeToShort(float f) {
        return (short) (f * Short.MAX_VALUE);
    }

    //____________________________________________________Listeners Para Eventos
    public interface INodeParser{
        public void onParseNode(SimpleNode node,int position);

    }

    public interface IHeaderNodeParser{

        public void onFirstHeaderNode(int children,int numInits);

        public void onParseHeaderNode(int position);

    }
    
}
